fix(dimentorin): verify-email validates OTP before activating user

- new app_otp_cache table + OtpCache entity (ResourceEnum::OtpCache)
- PostgresOtpRepository upsert/find/delete keyed by email
- register/resend persist otp_hash+expiry after email sent (no orphan OTP)
- verify_email validates via OtpManager::validate_otp_hash, single-use delete
- 8 unit tests pass, e2e verified: wrong OTP 400, correct OTP 200
This commit is contained in:
asepharyana
2026-08-04 23:35:34 +07:00
parent 3692b81324
commit c6ed5c5c19
12 changed files with 240 additions and 18 deletions
+43 -2
View File
@@ -27,14 +27,24 @@ impl OtpManager {
}
pub fn validate_otp(stored: &OtpData, user_otp: u32) -> bool {
if Utc::now() > stored.expires_at {
Self::validate_otp_hash(&stored.hash, &stored.expires_at, user_otp)
}
/// Validate a user-supplied OTP against a stored hash + expiry (e.g. from a
/// cache table where the plaintext code is not persisted).
pub fn validate_otp_hash(
stored_hash: &str,
expires_at: &DateTime<Utc>,
user_otp: u32,
) -> bool {
if Utc::now() > *expires_at {
return false;
}
let user_otp_str = user_otp.to_string();
let mut hasher = Sha256::new();
hasher.update(user_otp_str.as_bytes());
let user_hash = format!("{:x}", hasher.finalize());
user_hash == stored.hash
user_hash == stored_hash
}
}
@@ -63,6 +73,37 @@ mod tests {
assert!(!OtpManager::validate_otp(&otp, 123456));
}
#[test]
fn test_validate_otp_hash_valid() {
let otp = OtpManager::generate_otp();
assert!(OtpManager::validate_otp_hash(
&otp.hash,
&otp.expires_at,
otp.code
));
}
#[test]
fn test_validate_otp_hash_invalid() {
let otp = OtpManager::generate_otp();
assert!(!OtpManager::validate_otp_hash(
&otp.hash,
&otp.expires_at,
123456
));
}
#[test]
fn test_validate_otp_hash_expired() {
let mut otp = OtpManager::generate_otp();
otp.expires_at = Utc::now() - chrono::Duration::seconds(1);
assert!(!OtpManager::validate_otp_hash(
&otp.hash,
&otp.expires_at,
otp.code
));
}
#[test]
fn test_validate_otp_expired() {
let mut otp = OtpManager::generate_otp();