af2b300
use db::conn::sea_orm::{EntityTrait, QueryFilter};
use email::events::{EmailAddress, SendEmailRequest};
use crate::error::AuthError;
use crate::features::AuthService;
use super::types::{ResendCodeCmd, ResendCodeResp};
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(())
}
fn purpose_to_str(
p: db::user::user_email_verification_codes::EmailVerificationPurpose,
) -> &'static str {
use db::user::user_email_verification_codes::EmailVerificationPurpose as P;
match p {
P::Signup => "signup",
P::PasswordReset => "password_reset",
P::EmailChange => "email_change",
}
}
async fn send_resend_email(
svc: &email::EmailService,
email: &str,
code: &str,
purpose: db::user::user_email_verification_codes::EmailVerificationPurpose,
ttl_secs: u64,
) -> Result<(), AuthError> {
let subject = match purpose {
db::user::user_email_verification_codes::EmailVerificationPurpose::Signup => {
"Verify your account"
}
db::user::user_email_verification_codes::EmailVerificationPurpose::PasswordReset => {
"Reset your password"
}
db::user::user_email_verification_codes::EmailVerificationPurpose::EmailChange => {
"Confirm your new email"
}
};
let text = format!(
"Your verification code is: {}\n\nThe code expires in {} seconds.",
code, ttl_secs
);
let req = SendEmailRequest {
to: vec![EmailAddress::new(email)],
subject: subject.to_string(),
text: Some(text),
..Default::default()
};
svc.send(req).await?;
Ok(())
}
/// Re-issue a verification code for an already-registered user.
///
/// The response is always 200-shaped regardless of whether the email
/// exists, mirroring the forgot-password flow, to avoid leaking which
/// addresses are registered. Requires a fresh PoW solution.
pub async fn resend_code(
cmd: ResendCodeCmd,
svc: &AuthService,
expected_cid: Option<&str>,
) -> Result<ResendCodeResp, AuthError> {
verify_pow(&svc.pow, &cmd.pow, expected_cid).await?;
let db = svc.db.writer();
let lower = cmd.email.trim().to_ascii_lowercase();
let email_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())
.one(&db)
.await?;
// Always build a temp_key so the response shape is constant.
let ttl = svc.config.verify_code_ttl_secs;
let temp_id = uuid::Uuid::new_v4();
let temp_key = crate::features::TempKey {
name: format!(
"{}resend_{}_{}",
svc.config.cookie_name_prefix,
purpose_to_str(cmd.purpose),
lower.replace(|c: char| !c.is_ascii_alphanumeric(), "_")
),
value: temp_id.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()),
};
let mut sent = false;
if let Some(em) = email_row {
let artifacts = crate::features::email_code::usecase::issue_code(
crate::features::email_code::types::IssueCodeCmd {
email: cmd.email.clone(),
purpose: cmd.purpose,
user_id: em.user_id,
},
&db,
&svc.kv,
&svc.config,
)
.await?;
if let Err(e) =
send_resend_email(&svc.email, &cmd.email, &artifacts.code, cmd.purpose, ttl).await
{
tracing::warn!(?e, email = %cmd.email, "resend email send failed");
} else {
sent = true;
}
}
Ok(ResendCodeResp { sent, temp_key })
}