use crate::AppService; use crate::error::AppError; use argon2::{Argon2, PasswordHash, PasswordVerifier}; use models::users::{user_email, user_email_change, user_password}; use sea_orm::*; use serde::{Deserialize, Serialize}; use session::Session; use uuid::Uuid; #[derive(Debug, Clone, Deserialize, Serialize, utoipa::ToSchema)] pub struct EmailChangeRequest { pub new_email: String, pub password: String, } #[derive(Debug, Clone, Deserialize, Serialize, utoipa::ToSchema)] pub struct EmailVerifyRequest { pub token: String, } #[derive(Debug, Clone, Serialize, utoipa::ToSchema)] pub struct EmailResponse { pub email: Option, } impl AppService { /// Get the current email address for the authenticated user. pub async fn auth_get_email(&self, ctx: &Session) -> Result { let user_uid = ctx.user().ok_or(AppError::Unauthorized)?; let email = user_email::Entity::find() .filter(user_email::Column::User.eq(user_uid)) .one(&self.db) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; Ok(EmailResponse { email: email.map(|e| e.email), }) } /// Request an email change: validates password, stores a pending token, /// and sends a verification email to the new address. pub async fn auth_email_change_request( &self, ctx: &Session, params: EmailChangeRequest, ) -> Result<(), AppError> { let user_uid = ctx.user().ok_or(AppError::Unauthorized)?; // Verify password let password = self.auth_rsa_decode(ctx, params.password).await?; let user_password = user_password::Entity::find() .filter(user_password::Column::User.eq(user_uid)) .one(&self.db) .await .ok() .flatten() .ok_or(AppError::UserNotFound)?; let hash = PasswordHash::new(&user_password.password_hash).map_err(|_| AppError::UserNotFound)?; Argon2::default() .verify_password(password.as_bytes(), &hash) .map_err(|_| AppError::InvalidPassword)?; // Check new email is not already taken let existing = user_email::Entity::find() .filter(user_email::Column::Email.eq(¶ms.new_email)) .one(&self.db) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; if existing.is_some() { return Err(AppError::EmailExists); } // Generate token and store pending change let token = self.generate_reset_token(); let expires_at = chrono::Utc::now() + chrono::Duration::hours(24); let _ = user_email_change::Entity::delete_many() .filter(user_email_change::Column::UserUid.eq(user_uid)) .filter(user_email_change::Column::Used.eq(false)) .exec(&self.db) .await; user_email_change::ActiveModel { token: Set(token.clone()), user_uid: Set(user_uid), new_email: Set(params.new_email.clone()), expires_at: Set(expires_at), used: Set(false), created_at: Set(chrono::Utc::now()), } .insert(&self.db) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; // Queue verification email via Redis Stream let domain = self .config .main_domain() .map_err(|_| AppError::DoMainNotSet)?; let verify_link = format!("https://{}/auth/verify-email?token={}", domain, token); let envelope = queue::EmailEnvelope { id: Uuid::new_v4(), to: params.new_email.clone(), subject: "Confirm Email Change".to_string(), body: format!( "You have requested to change your email address.\n\n\ Please click the link below to confirm:\n\n{}\n\n\ This link will expire in 24 hours.\n\n\ If you did not request this change, please ignore this email.", verify_link ), created_at: chrono::Utc::now(), }; self.queue_producer .publish_email(envelope) .await .map_err(|e| AppError::InternalServerError(e.to_string()))?; slog::info!(self.logs, "Email change verification queued"; "new_email" => %params.new_email, "user_uid" => %user_uid); Ok(()) } /// Verify an email change token and apply the new email. pub async fn auth_email_verify(&self, params: EmailVerifyRequest) -> Result<(), AppError> { let change = user_email_change::Entity::find() .filter(user_email_change::Column::Token.eq(¶ms.token)) .filter(user_email_change::Column::Used.eq(false)) .one(&self.db) .await .map_err(|e| AppError::DatabaseError(e.to_string()))? .ok_or(AppError::NotFound("Invalid or expired token".to_string()))?; // Check expiry if change.expires_at < chrono::Utc::now() { return Err(AppError::NotFound("Token has expired".to_string())); } // Update or insert the new email in user_email let existing_email = user_email::Entity::find() .filter(user_email::Column::User.eq(change.user_uid)) .one(&self.db) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; match existing_email { Some(email_model) => { let mut active: user_email::ActiveModel = email_model.into(); active.email = Set(change.new_email.clone()); active.update(&self.db).await } None => { user_email::ActiveModel { user: Set(change.user_uid), email: Set(change.new_email.clone()), created_at: Set(chrono::Utc::now()), } .insert(&self.db) .await } } .map_err(|e| AppError::DatabaseError(e.to_string()))?; // Mark token as used let new_email = change.new_email.clone(); let user_uid = change.user_uid; let mut used_change: user_email_change::ActiveModel = change.into(); used_change.used = Set(true); used_change .update(&self.db) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; slog::info!(self.logs, "Email changed successfully"; "new_email" => %new_email, "user_uid" => %user_uid); Ok(()) } }