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
@@ -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(())
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod email_service;
pub use email_service::EmailService;