a06673b
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<ReclaimResp, AuthError> {
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 })
}