use db::conn::sea_orm::{EntityTrait, QueryFilter}; use db::user::user_email_verification_codes::EmailVerificationPurpose; use email::events::{EmailAddress, SendEmailRequest}; use crate::error::AuthError; use crate::features::AuthService; use super::types::{ReclaimCmd, ReclaimResp}; async fn verify_pow( pow: &crate::pow::PowService, proof: &crate::features::register::PowProof, expected_cid: Option<&str>, ) -> Result<(), AuthError> { let r = pow .verify_solution_once(&proof.challenge, &proof.solution, expected_cid) .await?; if !r.verified { return Err(AuthError::PowFailed("PoW solution not accepted".into())); } Ok(()) } async fn send_reclaim_email( svc: &email::EmailService, email: &str, code: &str, ttl_secs: u64, ) -> Result<(), AuthError> { let text = format!( "Your verification code is: {}\n\nThe code expires in {} seconds.\n\n\ If you did not request to recover this account, please ignore this email.", code, ttl_secs ); let req = SendEmailRequest { to: vec![EmailAddress::new(email)], subject: "Recover your account".to_string(), text: Some(text), ..Default::default() }; svc.send(req).await?; Ok(()) } /// Let the real owner of a mailbox reclaim an account that was registered but /// never activated (email squatting). A `Signup`-purpose code is issued only /// when the address exists and the account is still inactive; otherwise the /// response keeps a constant shape to avoid leaking which emails are /// registered or already active. Verification goes through the standard /// `/auth/email/verify` endpoint, whose successful `Signup` verification /// activates the account (B1). pub async fn reclaim( cmd: ReclaimCmd, svc: &AuthService, expected_cid: Option<&str>, ) -> Result { verify_pow(&svc.pow, &cmd.pow, expected_cid).await?; let db = svc.db.writer(); let lower = cmd.email.trim().to_ascii_lowercase(); let row = db::user::user_emails::Entity::find() .filter(db::user::user_emails::COLUMN.lower_email.eq(lower.clone())) .filter(db::user::user_emails::COLUMN.deleted_at.is_null()) .find_also_related(db::user::users::Entity) .one(&db) .await?; let ttl = svc.config.verify_code_ttl_secs; // Constant 200-shaped response: a code is only issued for inactive // accounts whose email row still exists. let mut sent = false; let mut temp_key = crate::features::TempKey { name: crate::features::email_code::usecase::cookie_name( &svc.config.cookie_name_prefix, EmailVerificationPurpose::Signup, &cmd.email, ), value: uuid::Uuid::new_v4().to_string(), ttl_seconds: ttl, secure: svc.config.cookie_secure, http_only: true, same_site: svc.config.cookie_same_site.clone(), domain: svc.config.cookie_domain.clone(), path: Some("/".to_string()), }; if let Some((em, Some(user))) = row && !user.is_active { let artifacts = crate::features::email_code::usecase::issue_code( crate::features::email_code::types::IssueCodeCmd { email: cmd.email.clone(), purpose: EmailVerificationPurpose::Signup, user_id: em.user_id, }, &db, &svc.kv, &svc.config, ) .await?; temp_key = artifacts.temp_key; if let Err(e) = send_reclaim_email(&svc.email, &cmd.email, &artifacts.code, ttl).await { tracing::warn!(?e, email = %cmd.email, "reclaim email send failed"); } else { sent = true; } } Ok(ReclaimResp { sent, temp_key }) }