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.
This commit is contained in:
2026-03-14 23:38:15 -03:00
parent f5f4e878b4
commit d5af338ee1
38 changed files with 1146 additions and 75 deletions
+172 -5
View File
@@ -23,6 +23,7 @@ Backend da aplicação MeowSpool escrito em **Rust**, utilizando **Axum** como f
| 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 |
@@ -41,7 +42,8 @@ backend/
├── migrations/ <- SQL puro, gerenciado pelo SQLx CLI
│ ├── 20240101000001_create_users.sql
│ ├── 20240101000002_create_spool_presets.sql
── 20240101000003_create_filaments.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
@@ -63,9 +65,14 @@ backend/
├── 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
@@ -119,9 +126,19 @@ Todas as rotas são prefixadas com `/api/v1`.
| 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) |
| 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) |
### Redirects para Deep Links — `/api/v1`
| 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`
@@ -204,9 +221,15 @@ pub enum AppError {
#[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),
}
@@ -311,7 +334,26 @@ Arquivos ficam em `backend/migrations/`. Nomeie com timestamp e descrição clar
## Variáveis de Ambiente
Copie `.env.example` para `.env` antes de rodar. Veja o arquivo `.env.example` para a lista completa.
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 |
---
@@ -447,6 +489,131 @@ GET /api/v1/filaments/:id/label.svg?width_mm=22&height_mm=14&fields=color,name,q
## 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`):
```rust
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:
```rust
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
---
### ✅ Redirect HTTP → Deep Link para links de e-mail
**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_handler` — `GET /api/v1/verify-email?token=xxx`
- `reset_password_redirect_handler` — `GET /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 TIMESTAMPTZ` — `NULL` = 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`