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:
@@ -1,4 +1,4 @@
|
||||
use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
|
||||
use axum::{extract::{Query, State}, http::{header, StatusCode}, response::IntoResponse, Json};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
@@ -40,11 +40,22 @@ pub struct ForgotPasswordRequest {
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate)]
|
||||
pub struct ResendVerificationRequest {
|
||||
#[validate(email)]
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct VerifyEmailRequest {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct TokenQueryParams {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate)]
|
||||
pub struct ResetPasswordRequest {
|
||||
pub token: String,
|
||||
@@ -68,15 +79,18 @@ pub async fn register_handler(
|
||||
Json(req): Json<RegisterRequest>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
req.validate()?;
|
||||
let tokens = state.auth_service.register(req.email, req.password).await?;
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
Json(AuthResponse {
|
||||
access_token: tokens.access_token,
|
||||
refresh_token: tokens.refresh_token,
|
||||
token_type: "Bearer",
|
||||
}),
|
||||
))
|
||||
let email = req.email.clone();
|
||||
let user = state.auth_service.register(req.email, req.password).await?;
|
||||
|
||||
// Envia email de verificação em background para não bloquear a resposta
|
||||
let ets = state.email_token_service.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = ets.send_verification_email(user.id, &email).await {
|
||||
tracing::warn!(error = %e, "Failed to send verification email");
|
||||
}
|
||||
});
|
||||
|
||||
Ok(StatusCode::CREATED)
|
||||
}
|
||||
|
||||
pub async fn login_handler(
|
||||
@@ -124,30 +138,66 @@ pub async fn logout_handler() -> impl IntoResponse {
|
||||
StatusCode::NO_CONTENT
|
||||
}
|
||||
|
||||
pub async fn resend_verification_handler(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<ResendVerificationRequest>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
req.validate()?;
|
||||
state.email_token_service.resend_verification_email(&req.email).await?;
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
pub async fn forgot_password_handler(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<ForgotPasswordRequest>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
req.validate()?;
|
||||
// TODO: gerar token de reset e enviar email
|
||||
// Retornamos sempre 200 para não vazar informação sobre emails cadastrados
|
||||
// Sempre retorna 200 para não vazar informação sobre emails cadastrados
|
||||
state.email_token_service.forgot_password(&req.email).await?;
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
pub async fn verify_email_handler(
|
||||
Json(_req): Json<VerifyEmailRequest>,
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<VerifyEmailRequest>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
// TODO: validar token e marcar email_verified = true
|
||||
state.email_token_service.verify_email(&req.token).await?;
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
pub async fn reset_password_handler(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<ResetPasswordRequest>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
req.validate()?;
|
||||
// TODO: validar token de reset e atualizar senha
|
||||
state.email_token_service.reset_password(&req.token, &req.new_password).await?;
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
/// GET /verify-email?token=xxx → redireciona para o deep link do app
|
||||
pub async fn verify_email_redirect_handler(
|
||||
State(state): State<AppState>,
|
||||
Query(params): Query<TokenQueryParams>,
|
||||
) -> impl IntoResponse {
|
||||
let location = format!(
|
||||
"{}://verify-email?token={}",
|
||||
state.config.app_scheme, params.token
|
||||
);
|
||||
(StatusCode::FOUND, [(header::LOCATION, location)])
|
||||
}
|
||||
|
||||
/// GET /reset-password?token=xxx → redireciona para o deep link do app
|
||||
pub async fn reset_password_redirect_handler(
|
||||
State(state): State<AppState>,
|
||||
Query(params): Query<TokenQueryParams>,
|
||||
) -> impl IntoResponse {
|
||||
let location = format!(
|
||||
"{}://reset-password?token={}",
|
||||
state.config.app_scheme, params.token
|
||||
);
|
||||
(StatusCode::FOUND, [(header::LOCATION, location)])
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helper: verificação de Google ID Token
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use axum::{extract::State, response::IntoResponse, Extension, Json};
|
||||
use axum::{extract::State, http::StatusCode, response::IntoResponse, Extension, Json};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
use uuid::Uuid;
|
||||
@@ -68,5 +68,18 @@ pub async fn update_me_handler(
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
req.validate()?;
|
||||
// TODO: implementar atualização de perfil
|
||||
Ok(axum::http::StatusCode::OK)
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
pub async fn delete_me_handler(
|
||||
State(state): State<AppState>,
|
||||
Extension(current_user): Extension<CurrentUser>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
state
|
||||
.auth_service
|
||||
.user_repo_ref()
|
||||
.delete(current_user.id)
|
||||
.await?;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
@@ -100,6 +100,15 @@ impl UserRepository for PostgresUserRepository {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
|
||||
sqlx::query("DELETE FROM users WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(self.db.as_ref())
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -60,16 +60,14 @@ impl AuthService {
|
||||
// Registro com email/senha
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
pub async fn register(&self, email: String, password: String) -> Result<TokenPair, AppError> {
|
||||
if let Some(_) = self.user_repo.find_by_email(&email).await? {
|
||||
pub async fn register(&self, email: String, password: String) -> Result<User, AppError> {
|
||||
if self.user_repo.find_by_email(&email).await?.is_some() {
|
||||
return Err(AppError::Conflict("email already registered".into()));
|
||||
}
|
||||
|
||||
let password_hash = hash_password(&password)?;
|
||||
let user = User::new_with_password(email, password_hash);
|
||||
let user = self.user_repo.create(&user).await?;
|
||||
|
||||
self.issue_token_pair(&user)
|
||||
let new_user = User::new_with_password(email, password_hash);
|
||||
Ok(self.user_repo.create(&new_user).await?)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -86,6 +84,10 @@ impl AuthService {
|
||||
let hash = user.password_hash.as_deref().ok_or(AppError::Unauthorized)?;
|
||||
verify_password(&password, hash)?;
|
||||
|
||||
if !user.email_verified {
|
||||
return Err(AppError::EmailNotVerified);
|
||||
}
|
||||
|
||||
self.issue_token_pair(&user)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
application::auth_service::hash_password,
|
||||
config::Config,
|
||||
error::AppError,
|
||||
infrastructure::EmailService,
|
||||
ports::UserRepository,
|
||||
};
|
||||
|
||||
pub struct EmailTokenService {
|
||||
db: Arc<PgPool>,
|
||||
user_repo: Arc<dyn UserRepository>,
|
||||
email_svc: Arc<EmailService>,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl EmailTokenService {
|
||||
pub fn new(
|
||||
db: Arc<PgPool>,
|
||||
user_repo: Arc<dyn UserRepository>,
|
||||
email_svc: Arc<EmailService>,
|
||||
config: Config,
|
||||
) -> Self {
|
||||
Self { db, user_repo, email_svc, config }
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Forgot password: gera token e envia email
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
pub async fn forgot_password(&self, email: &str) -> Result<(), AppError> {
|
||||
// Não revelamos se o email existe ou não
|
||||
let Some(user) = self.user_repo.find_by_email(email).await? else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let token = generate_token();
|
||||
let expires_at = time::OffsetDateTime::now_utc() + time::Duration::hours(1);
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO password_reset_tokens (user_id, token, expires_at) VALUES ($1, $2, $3)",
|
||||
)
|
||||
.bind(user.id)
|
||||
.bind(&token)
|
||||
.bind(expires_at)
|
||||
.execute(self.db.as_ref())
|
||||
.await?;
|
||||
|
||||
let reset_link = format!(
|
||||
"{}/reset-password?token={}",
|
||||
self.config.app_base_url, token
|
||||
);
|
||||
self.email_svc.send_password_reset(email, &reset_link).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Verify email: valida token e marca email_verified = true
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
pub async fn verify_email(&self, token: &str) -> Result<(), AppError> {
|
||||
let row = sqlx::query_as::<_, TokenRow>(
|
||||
"SELECT id, user_id, used_at, expires_at FROM email_verification_tokens WHERE token = $1",
|
||||
)
|
||||
.bind(token)
|
||||
.fetch_optional(self.db.as_ref())
|
||||
.await?
|
||||
.ok_or(AppError::BadRequest("Token inválido".into()))?;
|
||||
|
||||
validate_token(&row)?;
|
||||
|
||||
self.user_repo.verify_email(row.user_id).await?;
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE email_verification_tokens SET used_at = NOW() WHERE id = $1",
|
||||
)
|
||||
.bind(row.id)
|
||||
.execute(self.db.as_ref())
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Reset password: valida token e atualiza senha
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
pub async fn reset_password(&self, token: &str, new_password: &str) -> Result<(), AppError> {
|
||||
let row = sqlx::query_as::<_, TokenRow>(
|
||||
"SELECT id, user_id, used_at, expires_at FROM password_reset_tokens WHERE token = $1",
|
||||
)
|
||||
.bind(token)
|
||||
.fetch_optional(self.db.as_ref())
|
||||
.await?
|
||||
.ok_or(AppError::BadRequest("Token inválido".into()))?;
|
||||
|
||||
validate_token(&row)?;
|
||||
|
||||
let mut user = self
|
||||
.user_repo
|
||||
.find_by_id(row.user_id)
|
||||
.await?
|
||||
.ok_or(AppError::BadRequest("Usuário não encontrado".into()))?;
|
||||
|
||||
user.password_hash = Some(hash_password(new_password)?);
|
||||
self.user_repo.update(&user).await?;
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE password_reset_tokens SET used_at = NOW() WHERE id = $1",
|
||||
)
|
||||
.bind(row.id)
|
||||
.execute(self.db.as_ref())
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Geração de token de verificação de email (chamado no registro)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Reenvio de e-mail de verificação (endpoint público)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
pub async fn resend_verification_email(&self, email: &str) -> Result<(), AppError> {
|
||||
// Não revela se o e-mail existe ou se já foi verificado
|
||||
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
|
||||
}
|
||||
|
||||
pub async fn send_verification_email(&self, user_id: Uuid, email: &str) -> Result<(), AppError> {
|
||||
let token = generate_token();
|
||||
let expires_at = time::OffsetDateTime::now_utc() + time::Duration::hours(24);
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO email_verification_tokens (user_id, token, expires_at) VALUES ($1, $2, $3)",
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(&token)
|
||||
.bind(expires_at)
|
||||
.execute(self.db.as_ref())
|
||||
.await?;
|
||||
|
||||
let verify_link = format!(
|
||||
"{}/verify-email?token={}",
|
||||
self.config.app_base_url, token
|
||||
);
|
||||
self.email_svc.send_email_verification(email, &verify_link).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
fn generate_token() -> String {
|
||||
Uuid::new_v4().to_string().replace('-', "")
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct TokenRow {
|
||||
id: Uuid,
|
||||
user_id: Uuid,
|
||||
used_at: Option<time::OffsetDateTime>,
|
||||
expires_at: time::OffsetDateTime,
|
||||
}
|
||||
|
||||
fn validate_token(row: &TokenRow) -> Result<(), AppError> {
|
||||
if row.used_at.is_some() {
|
||||
return Err(AppError::BadRequest("Token já utilizado".into()));
|
||||
}
|
||||
if time::OffsetDateTime::now_utc() > row.expires_at {
|
||||
return Err(AppError::BadRequest("Token expirado".into()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
pub mod auth_service;
|
||||
pub mod email_token_service;
|
||||
pub mod filament_service;
|
||||
pub mod spool_preset_service;
|
||||
|
||||
pub use auth_service::AuthService;
|
||||
pub use email_token_service::EmailTokenService;
|
||||
pub use filament_service::FilamentService;
|
||||
pub use spool_preset_service::SpoolPresetService;
|
||||
|
||||
@@ -12,6 +12,14 @@ pub struct Config {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub app_env: AppEnv,
|
||||
// Email (SMTP)
|
||||
pub smtp_host: String,
|
||||
pub smtp_port: u16,
|
||||
pub smtp_user: String,
|
||||
pub smtp_pass: String,
|
||||
pub email_from: String,
|
||||
pub app_base_url: String,
|
||||
pub app_scheme: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -32,6 +40,13 @@ impl Config {
|
||||
host: optional_string_default("HOST", "0.0.0.0"),
|
||||
port: optional("PORT", 8080)?,
|
||||
app_env: parse_app_env(),
|
||||
smtp_host: optional_string_default("SMTP_HOST", "smtp.gmail.com"),
|
||||
smtp_port: optional("SMTP_PORT", 587)?,
|
||||
smtp_user: optional_string("SMTP_USER"),
|
||||
smtp_pass: optional_string("SMTP_PASS"),
|
||||
email_from: optional_string_default("EMAIL_FROM", "noreply@meowspool.app"),
|
||||
app_base_url: optional_string_default("APP_BASE_URL", "http://localhost:8080"),
|
||||
app_scheme: optional_string_default("APP_SCHEME", "meowspool"),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,15 @@ pub enum AppError {
|
||||
#[error("forbidden")]
|
||||
Forbidden,
|
||||
|
||||
#[error("email not verified")]
|
||||
EmailNotVerified,
|
||||
|
||||
#[error("validation error: {0}")]
|
||||
Validation(String),
|
||||
|
||||
#[error("bad request: {0}")]
|
||||
BadRequest(String),
|
||||
|
||||
#[error("conflict: {0}")]
|
||||
Conflict(String),
|
||||
|
||||
@@ -37,7 +43,9 @@ impl AppError {
|
||||
Self::NotFound => (StatusCode::NOT_FOUND, "NOT_FOUND"),
|
||||
Self::Unauthorized => (StatusCode::UNAUTHORIZED, "UNAUTHORIZED"),
|
||||
Self::Forbidden => (StatusCode::FORBIDDEN, "FORBIDDEN"),
|
||||
Self::EmailNotVerified => (StatusCode::FORBIDDEN, "EMAIL_NOT_VERIFIED"),
|
||||
Self::Validation(_) => (StatusCode::BAD_REQUEST, "VALIDATION_ERROR"),
|
||||
Self::BadRequest(_) => (StatusCode::BAD_REQUEST, "BAD_REQUEST"),
|
||||
Self::Conflict(_) => (StatusCode::CONFLICT, "CONFLICT"),
|
||||
Self::UnprocessableEntity(_) => (StatusCode::UNPROCESSABLE_ENTITY, "UNPROCESSABLE_ENTITY"),
|
||||
Self::Internal(_) => (StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR"),
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
use lettre::{
|
||||
message::header::ContentType,
|
||||
transport::smtp::authentication::Credentials,
|
||||
AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor,
|
||||
};
|
||||
|
||||
use crate::{config::Config, error::AppError};
|
||||
|
||||
pub struct EmailService {
|
||||
mailer: AsyncSmtpTransport<Tokio1Executor>,
|
||||
from: String,
|
||||
}
|
||||
|
||||
impl EmailService {
|
||||
pub fn new(config: &Config) -> Result<Self, AppError> {
|
||||
let creds = Credentials::new(config.smtp_user.clone(), config.smtp_pass.clone());
|
||||
|
||||
let mailer = AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&config.smtp_host)
|
||||
.map_err(|e| AppError::Internal(anyhow::anyhow!("SMTP config error: {e}")))?
|
||||
.port(config.smtp_port)
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
Ok(Self {
|
||||
mailer,
|
||||
from: config.email_from.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn send_password_reset(&self, to: &str, reset_link: &str) -> Result<(), AppError> {
|
||||
let body = format!(
|
||||
"Você solicitou a redefinição de senha do MeowSpool.\n\nClique no link abaixo para redefinir sua senha (válido por 1 hora):\n\n{reset_link}\n\nSe você não solicitou isso, ignore este email."
|
||||
);
|
||||
|
||||
self.send(to, "Redefinição de senha — MeowSpool", &body).await
|
||||
}
|
||||
|
||||
pub async fn send_email_verification(&self, to: &str, verify_link: &str) -> Result<(), AppError> {
|
||||
let body = format!(
|
||||
"Bem-vindo ao MeowSpool! Confirme seu endereço de email clicando no link abaixo:\n\n{verify_link}\n\nO link expira em 24 horas."
|
||||
);
|
||||
|
||||
self.send(to, "Confirme seu email — MeowSpool", &body).await
|
||||
}
|
||||
|
||||
async fn send(&self, to: &str, subject: &str, body: &str) -> Result<(), AppError> {
|
||||
let email = Message::builder()
|
||||
.from(
|
||||
self.from
|
||||
.parse()
|
||||
.map_err(|e| AppError::Internal(anyhow::anyhow!("Invalid from address: {e}")))?,
|
||||
)
|
||||
.to(to
|
||||
.parse()
|
||||
.map_err(|e| AppError::Internal(anyhow::anyhow!("Invalid to address: {e}")))?)
|
||||
.subject(subject)
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(body.to_string())
|
||||
.map_err(|e| AppError::Internal(anyhow::anyhow!("Email build error: {e}")))?;
|
||||
|
||||
AsyncTransport::send(&self.mailer, email)
|
||||
.await
|
||||
.map_err(|e| AppError::Internal(anyhow::anyhow!("SMTP send error: {e}")))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod email_service;
|
||||
|
||||
pub use email_service::EmailService;
|
||||
@@ -8,6 +8,7 @@ mod application;
|
||||
mod config;
|
||||
mod domain;
|
||||
mod error;
|
||||
mod infrastructure;
|
||||
mod ports;
|
||||
mod router;
|
||||
|
||||
|
||||
@@ -13,4 +13,5 @@ pub trait UserRepository: Send + Sync {
|
||||
async fn create(&self, user: &User) -> Result<User, AppError>;
|
||||
async fn update(&self, user: &User) -> Result<User, AppError>;
|
||||
async fn verify_email(&self, id: Uuid) -> Result<(), AppError>;
|
||||
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
||||
}
|
||||
|
||||
+19
-2
@@ -18,8 +18,9 @@ use crate::{
|
||||
PostgresUserRepository,
|
||||
},
|
||||
},
|
||||
application::{AuthService, FilamentService, SpoolPresetService},
|
||||
application::{AuthService, EmailTokenService, FilamentService, SpoolPresetService},
|
||||
config::Config,
|
||||
infrastructure::EmailService,
|
||||
ports::{FilamentRepository, SpoolPresetRepository, UserRepository},
|
||||
};
|
||||
|
||||
@@ -27,6 +28,7 @@ use crate::{
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub auth_service: Arc<AuthService>,
|
||||
pub email_token_service: Arc<EmailTokenService>,
|
||||
pub filament_service: Arc<FilamentService>,
|
||||
pub spool_preset_service: Arc<SpoolPresetService>,
|
||||
pub config: Config,
|
||||
@@ -46,6 +48,15 @@ pub fn build(db: PgPool, config: Config) -> Router {
|
||||
Arc::clone(&user_repo),
|
||||
config.clone(),
|
||||
));
|
||||
let email_svc = Arc::new(
|
||||
EmailService::new(&config).expect("Failed to configure SMTP"),
|
||||
);
|
||||
let email_token_service = Arc::new(EmailTokenService::new(
|
||||
Arc::clone(&db),
|
||||
Arc::clone(&user_repo),
|
||||
Arc::clone(&email_svc),
|
||||
config.clone(),
|
||||
));
|
||||
let filament_service = Arc::new(FilamentService::new(
|
||||
Arc::clone(&filament_repo),
|
||||
Arc::clone(&preset_repo),
|
||||
@@ -54,6 +65,7 @@ pub fn build(db: PgPool, config: Config) -> Router {
|
||||
|
||||
let state = AppState {
|
||||
auth_service,
|
||||
email_token_service,
|
||||
filament_service,
|
||||
spool_preset_service,
|
||||
config: config.clone(),
|
||||
@@ -65,9 +77,13 @@ pub fn build(db: PgPool, config: Config) -> Router {
|
||||
.route("/auth/login", post(auth_handler::login_handler))
|
||||
.route("/auth/oauth/google", post(auth_handler::google_oauth_handler))
|
||||
.route("/auth/refresh", post(auth_handler::refresh_token_handler))
|
||||
.route("/auth/resend-verification", post(auth_handler::resend_verification_handler))
|
||||
.route("/auth/forgot-password", post(auth_handler::forgot_password_handler))
|
||||
.route("/auth/verify-email", post(auth_handler::verify_email_handler))
|
||||
.route("/auth/reset-password", post(auth_handler::reset_password_handler));
|
||||
.route("/auth/reset-password", post(auth_handler::reset_password_handler))
|
||||
// Redirects para deep links (usados nos links de email)
|
||||
.route("/verify-email", get(auth_handler::verify_email_redirect_handler))
|
||||
.route("/reset-password", get(auth_handler::reset_password_redirect_handler));
|
||||
|
||||
// Rotas protegidas (requerem JWT válido)
|
||||
let protected_routes = Router::new()
|
||||
@@ -75,6 +91,7 @@ pub fn build(db: PgPool, config: Config) -> Router {
|
||||
// Users
|
||||
.route("/users/me", get(user_handler::get_me_handler))
|
||||
.route("/users/me", put(user_handler::update_me_handler))
|
||||
.route("/users/me", delete(user_handler::delete_me_handler))
|
||||
// Dashboard
|
||||
.route("/dashboard", get(filament_handler::dashboard_handler))
|
||||
// Filaments
|
||||
|
||||
Reference in New Issue
Block a user