- Added support for Google OAuth with separate client IDs for Android and iOS. - Updated `verify_google_id_token` to validate `aud` against both client IDs and check `email_verified`. - Modified `google_oauth_handler` to accept and process the new client IDs. - Enhanced security by enforcing explicit JWT algorithm validation. - Updated mobile app to handle Google OAuth flow using `expo-auth-session`. - Fixed API request to send `id_token` in snake_case as expected by the backend. - Added necessary environment variables for Google client IDs in mobile app. - Implemented intent filter for Google OAuth redirect in AndroidManifest.xml.
26 KiB
MeowSpool — Documento de Referência do Projeto
Canvas de Design: https://app.paper.design/file/01KKM0HA6NBV44VCQDFQHC2MRY
1. Visão Geral
MeowSpool é uma solução para entusiastas e profissionais de impressão 3D gerenciarem seu estoque de filamentos. O diferencial está na precisão do cálculo de peso líquido (descontando o peso do carretel via presets), na identidade visual inspirada em gatos siameses, e na integração com etiquetas físicas exportáveis (SVG/PDF/QR Code/NFC).
Foco do desenvolvimento atual: App Mobile (React Native + Expo) + Backend em Rust. A interface Web é planejada para uma fase posterior, após a estabilização do mobile e da API.
2. Objetivos
- Precisão: Eliminar o "chute" de quanto filamento resta no rolo.
- Agilidade: Facilitar o cadastro e a consulta via dispositivos móveis, mesmo offline.
- Organização: Centralizar parâmetros técnicos (temperatura/fluxo) para consulta rápida.
3. Público-Alvo
- Hobbistas de impressão 3D.
- Donos de "Print Farms" (fazendas de impressão).
- Projetistas que trabalham com diversos materiais (PLA, ABS, PETG, etc).
4. Identidade Visual
4.1 Nome e Conceito
MeowSpool — fusão de "Meow" (gato) e "Spool" (carretel). A identidade visual é inspirada no gato siamês: tons quentes e escuros com detalhes em azul-aço que remetem aos olhos característicos da raça.
App é dark mode exclusivo — nenhum suporte a light mode.
4.2 Paleta de Cores
| Token | Hex | Função na Interface |
|---|---|---|
bgBase |
#1E1B18 |
Fundo geral |
bgSurface |
#2A2622 |
Cards, contêineres, widgets |
bgHover |
#332F2B |
Estados de hover em cards e itens de lista |
textPrimary |
#F5EEDC |
Títulos, peso líquido (texto importante) |
textSecondary |
#C9C1B0 |
Descrições, labels (material, marca) |
accent |
#38BCC2 |
Botões de ação, elementos ativos |
accentMuted |
#38BCC226 |
Backgrounds de badges e status (10% opacidade) |
stockLow |
#FF6B6B |
≤ 15% — vermelho |
stockMedium |
#FF9F43 |
≤ 35% — laranja |
stockOk |
#38BCC2 |
> 35% — accent |
border |
#3D3830 |
Bordas sutis |
error |
#FF6B6B |
Mensagens de erro |
4.3 Tipografia
- UI geral: Inter
- Slugs, hex, URLs, código: JetBrains Mono
5. Requisitos Funcionais (MVP)
5.1 Autenticação
- Métodos suportados: Email/senha e OAuth via Google.
- Verificação de e-mail obrigatória antes do primeiro login.
- Sessão: Token JWT com refresh token. O app mobile persiste a sessão localmente para acesso offline.
- Fluxos: Login, Cadastro, Verificar E-mail, Reenviar Verificação, Esqueci a Senha, Redefinir Senha.
5.2 Gestão de Inventário
- Cadastro de Filamento: Material, Marca, Modelo, Cor (hex), Temperatura Hotend, Temperatura Mesa, Fator de Fluxo/Extrusão, Notas.
- Calculadora de Peso Líquido:
Peso_Líquido = Peso_Total - Peso_Carretel(via preset selecionado). - Filtros: Material, marca, busca textual, nível de estoque (
low/medium/ok). - Ordenação: Peso líquido asc/desc, criado em desc (padrão).
5.3 Presets de Carretéis
Presets do Sistema (built-in):
| Nome do Preset | Peso do Carretel |
|---|---|
| Bambu Lab (Plástico) | 250g |
| Elegoo (Papelão) | 200g |
| Creality (Plástico) | 230g |
| Prusament (Plástico) | 201g |
| Sunlu (Papelão) | 200g |
| Polymaker (Plástico) | 220g |
| Genérico Papelão 1kg | 200g |
| Genérico Plástico 1kg | 250g |
Presets Customizados: O usuário pode criar, editar e excluir seus próprios presets. Presets do sistema são somente leitura.
5.4 Identificação e Etiquetas
- QR Code: Cada filamento gera QR Code com deep link
meowspool://filament/<id>. - Etiqueta SVG:
GET /filaments/:id/label.svg— parâmetroswidth_mm,height_mm,fields,dpi. - Etiqueta PDF:
GET /filaments/:id/label.pdf— mesmos parâmetros. - Impressoras suportadas: Padrão (50×30mm), Niimbot D11/D110 (22×14mm, dpi=203), Brother/Dymo (dpi=300).
- Campos selecionáveis:
color,name,material_brand,net_weight,print_temp,qrcode. - NFC: Leitura e gravação de tags NTAG215 com URI
meowspool://filament/<id>.
5.5 Dashboard
- Total de estoque em kg.
- Contagem de filamentos com estoque baixo.
- Resumo por material (contagem + peso total).
- Filamentos com estoque baixo (≤15%).
- Filamentos adicionados recentemente.
6. Arquitetura do Sistema
Ambos os lados seguem Arquitetura Hexagonal (Ports & Adapters):
adapters/inbound (HTTP / Tela)
|
v
application (Use Cases / Services)
|
v
domain (Entidades puras)
|
^
ports (Interfaces/traits)
|
^
adapters/outbound (PostgreSQL / SQLite / NFC / HTTP)
Regra fundamental: O domain e os ports nunca importam de adapters ou application.
7. Backend (Rust)
7.1 Stack
| Componente | Biblioteca |
|---|---|
| HTTP Framework | axum 0.7 |
| Async Runtime | tokio (full) |
| ORM / Query | sqlx (postgres, uuid, time, macros) |
| Autenticação JWT | jsonwebtoken 9.x |
| Hash de senha | argon2 0.5 (Argon2id) |
| OAuth Google | oauth2 4.x |
| Serialização | serde, serde_json |
| Erros | thiserror |
| Env vars | dotenvy |
| Logging | tracing, tracing-subscriber |
| Validação | validator |
| HTTP Client | reqwest |
| Email (SMTP) | lettre (STARTTLS) |
| Geração QR Code | qrcode + image |
| Geração PDF | printpdf 0.7 |
7.2 Estrutura de Pastas
backend/
├── Cargo.toml
├── migrations/
│ ├── 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: config, DB, router, 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/ ← entidades puras (User, Filament, SpoolPreset)
├── ports/ ← traits: UserRepository, FilamentRepository, SpoolPresetRepository
├── application/
│ ├── auth_service.rs ← login, register, OAuth, refresh, logout
│ ├── email_token_service.rs← forgot_password, verify_email, reset_password
│ ├── filament_service.rs ← CRUD, peso líquido, QR Code, SVG, PDF
│ └── spool_preset_service.rs
├── infrastructure/
│ └── email_service.rs ← SMTP via lettre
└── adapters/
├── inbound/ ← handlers Axum + middleware JWT
└── outbound/ ← implementações PostgreSQL dos ports
7.3 Rotas da API
Base: /api/v1
Auth — /api/v1/auth
| Método | Rota | Acesso |
|---|---|---|
| POST | /register |
Público — retorna 201 sem token |
| POST | /login |
Público |
| POST | /oauth/google |
Público |
| POST | /refresh |
Público (requer refresh token) |
| POST | /logout |
Autenticado |
| POST | /resend-verification |
Público — sempre retorna 200 |
| POST | /forgot-password |
Público |
| POST | /verify-email |
Público |
| POST | /reset-password |
Público (requer token) |
Redirects para Deep Links
| Método | Rota | Descrição |
|---|---|---|
| GET | /verify-email |
Redireciona 302 → {APP_SCHEME}://verify-email?token=xxx |
| GET | /reset-password |
Redireciona 302 → {APP_SCHEME}://reset-password?token=xxx |
Filaments — /api/v1/filaments
| Método | Rota | Acesso |
|---|---|---|
| GET | / |
Autenticado |
| POST | / |
Autenticado |
| GET | /:id |
Autenticado |
| PUT | /:id |
Autenticado |
| DELETE | /:id |
Autenticado |
| GET | /:id/qrcode |
Autenticado |
| GET | /:id/label.svg |
Autenticado |
| GET | /:id/label.pdf |
Autenticado |
Spool Presets — /api/v1/spool-presets
| Método | Rota | Acesso |
|---|---|---|
| GET | / |
Autenticado |
| POST | / |
Autenticado |
| PUT | /:id |
Autenticado (apenas presets do user) |
| DELETE | /:id |
Autenticado (apenas presets do user) |
Outros
| Método | Rota | Acesso |
|---|---|---|
| GET | /dashboard |
Autenticado |
| GET | /users/me |
Autenticado |
| PUT | /users/me |
Autenticado |
7.4 Padrão de Erros
{ "error": "not found", "code": "NOT_FOUND" }
| Variante AppError | HTTP | Code |
|---|---|---|
NotFound |
404 | NOT_FOUND |
Unauthorized |
401 | UNAUTHORIZED |
Forbidden |
403 | FORBIDDEN |
EmailNotVerified |
403 | EMAIL_NOT_VERIFIED |
Conflict |
409 | CONFLICT |
BadRequest |
400 | BAD_REQUEST |
Validation |
422 | VALIDATION_ERROR |
Internal |
500 | INTERNAL_ERROR |
7.5 Variáveis de Ambiente
| Variável | Obrigatória | Padrão |
|---|---|---|
DATABASE_URL |
✅ | — |
JWT_SECRET |
✅ | — |
JWT_EXPIRY_SECS |
❌ | 3600 |
JWT_REFRESH_EXPIRY_SECS |
❌ | 2592000 |
GOOGLE_CLIENT_ID |
❌ | — |
GOOGLE_CLIENT_SECRET |
❌ | — |
HOST / PORT |
❌ | 0.0.0.0 / 8080 |
SMTP_HOST / SMTP_PORT |
❌ | smtp.gmail.com / 587 |
SMTP_USER / SMTP_PASS |
❌ | — |
EMAIL_FROM |
❌ | noreply@meowspool.app |
APP_BASE_URL |
❌ | http://localhost:8080 |
APP_SCHEME |
❌ | meowspool |
Em produção:
APP_BASE_URL=https://meowspool.felipecncloud.com/api/v1
7.6 Como Rodar Localmente
cd backend/
cp .env.example .env
# 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
sqlx migrate run
cargo watch -x run
8. Mobile (React Native + Expo)
8.1 Stack
| Componente | Biblioteca |
|---|---|
| Framework | expo ~54 (Bare Workflow) |
| Navigation | expo-router ~4 (file-system) |
| Linguagem | TypeScript 5.x (strict) |
| Node Version | 22 LTS (via mise) |
| Java Version | 17 (obrigatório — Java 24 quebra CMake) |
| Forms | react-hook-form + zod |
| Estado global | zustand |
| HTTP | axios (interceptor Bearer + refresh) |
| DB local | expo-sqlite + SQLCipher |
| Auth persistência | expo-secure-store |
| UI | React Native + Ionicons |
| QR Code render | react-native-qrcode-svg |
| SVG inline | react-native-svg (SvgXml) |
| Câmera / Scanner | expo-camera ~17 (ML Kit) |
| NFC | react-native-nfc-manager |
| File system | expo-file-system/legacy |
| Compartilhamento | expo-sharing (iOS) |
| Abrir arquivo | expo-intent-launcher (Android) |
| Animações | react-native-reanimated |
Bare Workflow: o diretório
android/é comitado. Qualquer pacote com módulo nativo exigenpx expo prebuild --platform android+ rebuild do APK.
8.2 Estrutura de Pastas
mobile/
├── .mise.toml ← node 22 LTS, java 17
├── app.json ← Expo config, scheme "meowspool"
├── app/ ← expo-router (file-system routes)
│ ├── _layout.tsx ← Root layout (auth guard + sessão)
│ ├── (auth)/
│ │ ├── login.tsx ← G3-0
│ │ ├── register.tsx ← IX-0
│ │ ├── forgot-password.tsx ← L2-0
│ │ ├── check-email.tsx ← RU-0 (pós-cadastro / e-mail não verificado)
│ │ ├── reset-password.tsx ← 117-0
│ │ └── password-reset-done.tsx ← 135-0
│ ├── filament/[id].tsx ← Deep link: meowspool://filament/<id> → /(app)/inventory/<id>
│ ├── verify-email.tsx ← Deep link: meowspool://verify-email?token=xxx
│ └── (app)/
│ ├── (tabs)/
│ │ ├── home.tsx ← 1-0 Dashboard
│ │ ├── inventory.tsx ← CW-0 Inventário
│ │ ├── add.tsx ← FAB → /inventory/new
│ │ ├── config.tsx ← M4-0 Presets de Carretéis
│ │ └── profile.tsx ← 1LQ-0 Perfil
│ ├── inventory/
│ │ ├── new.tsx ← 2X-0 Cadastro
│ │ ├── filters.tsx ← 17H-0 Filtros (bottom sheet)
│ │ ├── [id].tsx ← 6L-0 Detalhe
│ │ └── [id]/edit.tsx ← 13O-0 Editar
│ ├── config/presets/
│ │ ├── new.tsx ← QM-0 Novo Preset
│ │ └── [id]/edit.tsx ← 1KD-0 Editar Preset
│ ├── scanner.tsx ← Scanner QR Code (ML Kit)
│ ├── nfc-reader.tsx ← 1RY-0 Leitura NFC (bottom sheet modal)
│ └── filaments/[id]/
│ ├── qrcode.tsx ← Ver QR Code
│ ├── label.tsx ← Exportar Etiqueta (PDF/SVG)
│ └── write-nfc.tsx ← Gravar tag NTAG215
└── src/
├── domain/ ← Entidades puras (Filament, SpoolPreset, User, Dashboard)
├── ports/ ← Interfaces (IFilamentRepository, ISpoolPresetRepository, IAuthRepository, INFCRepository)
├── application/ ← Use Cases (filament/, preset/, auth/, nfc/)
├── adapters/
│ ├── remote/ ← Axios → API Rust (ApiFilamentRepository, etc.)
│ ├── local/ ← SQLite (LocalFilamentRepository, LocalSpoolPresetRepository)
│ └── nfc/ ← react-native-nfc-manager (NFCManagerRepository)
├── store/ ← Zustand: authStore, filamentStore, presetStore
├── shared/ ← theme.ts, constants.ts, utils/
└── presentation/components/
├── ui/ ← Button, Input, Card, Badge
├── layout/ ← Screen, Header
└── filament/ ← StockBar, ColorSwatch, FilamentCard
8.3 Navegação e Deep Links
- Scheme:
meowspool://(configurado emapp.json) - Grupos:
(auth)— sem sessão |(app)— autenticado |(app)/(tabs)— bottom tabs
| Deep Link | Handler | Ação |
|---|---|---|
meowspool://filament/<id> |
app/filament/[id].tsx |
Redireciona para /(app)/inventory/<id> |
meowspool://verify-email?token=<tok> |
app/verify-email.tsx |
Verifica e-mail via POST /auth/verify-email |
meowspool://reset-password?token=<tok> |
app/(auth)/reset-password.tsx |
Exibe formulário de nova senha |
Links de e-mail: o backend gera
https://meowspool.felipecncloud.com/api/v1/verify-email?token=xxx(aceito por clientes de e-mail). O backend redireciona 302 →meowspool://.... O OS abre o app.
8.4 Estratégia Offline-First
- Escrita: toda mutação persiste primeiro no SQLite (
synced = 0) e enfileira emsync_queue. - Leitura: sempre do SQLite; a API é usada apenas para sync.
- Sync: background job consome
sync_queuee envia para o backend. - Conflito: last-write-wins via
updatedAt. O backend é a fonte de verdade. - Auth offline: token no
expo-secure-store; refresh automático via interceptor Axios.
Schema SQLite:
-- filaments
id TEXT PRIMARY KEY, user_id TEXT, material TEXT, brand TEXT, model TEXT,
color_hex TEXT, spool_preset_id TEXT, total_weight_g REAL, net_weight_g REAL,
temp_hotend_c REAL, temp_bed_c REAL, flow_factor_pct REAL, notes TEXT,
synced INTEGER DEFAULT 0, updated_at TEXT, created_at TEXT
-- spool_presets
id TEXT PRIMARY KEY, user_id TEXT, name TEXT, spool_weight_g REAL,
is_system INTEGER DEFAULT 0, synced INTEGER DEFAULT 0, created_at TEXT
-- sync_queue
id INTEGER PRIMARY KEY AUTOINCREMENT, entity_type TEXT, entity_id TEXT,
operation TEXT, payload TEXT, created_at TEXT
8.5 Como Rodar Localmente
cd mobile/
mise install # garante Node 22 e Java 17
npm install
# Criar .env
echo "EXPO_PUBLIC_API_URL=http://localhost:3000/api/v1" > .env
# Dev (Expo Go — sem câmera ML Kit / NFC)
npx expo start
# Build nativo Android (necessário para câmera ML Kit e NFC)
mise exec -- npx expo run:android
Atenção ao build Android: o projeto usa
namespace "com.meowspool"nobuild.gradle, mas o código fonte fica emcom.meowspool.app. Use sempre nomes totalmente qualificados noAndroidManifest.xml.
9. Estrutura de Dados (Entidades Principais)
users
| Campo | Tipo | Descrição |
|---|---|---|
| id | UUID | Identificador único |
| string | Email do usuário | |
| password_hash | string? | Hash Argon2id (nulo se OAuth) |
| google_id | string? | ID OAuth Google (opcional) |
| email_verified | boolean | Obrigatório para login |
| created_at | timestamp |
spool_presets
| Campo | Tipo | Descrição |
|---|---|---|
| id | UUID | Identificador único |
| name | string | Nome do preset |
| spool_weight_g | integer | Peso do carretel vazio em gramas |
| is_system | boolean | true = built-in (read-only para usuários) |
| user_id | UUID? | Nulo para presets do sistema |
| created_at | timestamp |
filaments
| Campo | Tipo | Descrição |
|---|---|---|
| id | UUID | Identificador único |
| user_id | UUID | Dono do filamento |
| material | string | PLA, ABS, PETG, TPU, ASA, PA, PC… |
| brand | string | Marca |
| model | string? | Modelo/linha |
| color_hex | string | Cor em hex (#FF5733) |
| spool_preset_id | UUID | Preset de carretel usado |
| total_weight_g | real | Peso total medido na balança (gramas) |
| net_weight_g | real | Calculado: total − peso do carretel |
| temp_hotend_c | real? | Temperatura do hotend (°C) |
| temp_bed_c | real? | Temperatura da mesa (°C) |
| flow_factor_pct | real? | Fator de fluxo (%) |
| notes | text? | Observações livres |
| updated_at | timestamp | Usado para resolução de conflitos sync |
| created_at | timestamp |
password_reset_tokens / email_verification_tokens
| Campo | Tipo | Descrição |
|---|---|---|
| id | UUID | PK |
| token | TEXT UNIQUE | Token único (indexado) |
| user_id | UUID | FK para users |
| expires_at | TIMESTAMPTZ | TTL: 1h (reset) / 24h (verificação) |
| used_at | TIMESTAMPTZ | NULL = não usado; preenchido ao usar |
10. Fluxo do Usuário
Cadastro e Login
- Usuário se cadastra → backend envia e-mail de verificação (background).
- Usuário clica no link
https://...→ backend redireciona 302 → deep link → app verifica. - Login com e-mail não verificado →
403 EMAIL_NOT_VERIFIED→ app sugere reenvio. - Login com e-mail verificado → tokens JWT emitidos → sessão persistida no SecureStore.
Cadastro de Filamento
- Usuário abre o App (autenticado) → "Novo Filamento".
- Preenche marca, material, modelo, seleciona cor hex.
- Seleciona preset de carretel ou cria um novo.
- Digita o peso total (ex: 850g) → app calcula: 600g disponível.
- Salva localmente (offline-first) e sincroniza quando online.
- QR Code e etiqueta ficam disponíveis imediatamente.
Identificação via QR Code / NFC
- Usuário aponta câmera para QR Code ou aproxima tag NFC do carretel.
- App lê
meowspool://filament/<id>→ navega direto para o detalhe do filamento.
11. Artboards de Design
| ID | Tela | Seção |
|---|---|---|
G3-0 |
Login | 5.1 |
IX-0 |
Cadastro | 5.1 |
L2-0 |
Recuperação de Senha | 5.1 |
RU-0 |
Verificação de Email | 5.1 |
117-0 |
Redefinir Senha | 5.1 |
135-0 |
Senha Redefinida | 5.1 |
1-0 |
Home / Dashboard | 5.5 |
CW-0 |
Inventário | 5.2 |
17H-0 |
Filtros (bottom sheet) | 5.2 |
2X-0 |
Cadastro de Filamento | 5.2 |
13O-0 |
Editar Filamento | 5.2 |
6L-0 |
Detalhe do Filamento | 5.2 |
M4-0 |
Presets de Carretéis | 5.3 |
QM-0 |
Novo Preset | 5.3 |
1KD-0 |
Editar Preset | 5.3 |
1CF-0 |
Ver QR Code | 5.4 |
1FZ-0 |
Exportar Etiqueta | 5.4 |
1RY-0 |
Leitura NFC (modal) | 5.4 |
1LQ-0 |
Perfil / Conta | 5.1 |
12. Roadmap
Pendente — Mobile
- Sync background com
sync_queueSQLite → API - Google OAuth com
expo-auth-session - NFC suporte iOS (entitlement
com.apple.developer.nfc.readwrite) - Expo Notifications para alertas de estoque baixo
- Testes de integração com Jest + Testing Library
Pendente — Infraestrutura
- DI container formal (instanciar e injetar repositórios concretos)
Fase 2 — Web (planejado)
- Front-end Web com Next.js (dashboard + inventário)
- Cache com Redis / HTTP Cache
- Histórico de uso por projeto/impressão
- Custo por grama
- Importação em lote (CSV/JSON)
- Pool público de presets compartilhados pela comunidade
Concluído
- CRUD de filamentos (mobile + API)
- CRUD de presets (mobile + API)
- Dashboard (mobile + API)
- QR Code geração e scanner (ML Kit)
- Exportação de etiqueta SVG e PDF
- Suporte Niimbot D11/D110 (22×14mm) com layout adaptativo
- NFC leitura e gravação NTAG215 (Android)
- Deep links (filamento, verificação de e-mail, reset de senha)
- Verificação de e-mail obrigatória + reenvio
- Fluxo completo de reset de senha
- Redirect HTTP → deep link (compatibilidade com clientes de e-mail)