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:
+172
-5
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user