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:
@@ -0,0 +1,84 @@
|
||||
use crate::auth::domain::otp::{OtpCacheRecord, OtpRepository};
|
||||
use async_trait::async_trait;
|
||||
use chrono::Utc;
|
||||
use imphnen_entities::seaorm::common::otp_cache::{
|
||||
ActiveModel as OtpCacheActiveModel, Column as OtpCacheColumn, Entity as OtpCacheEntity,
|
||||
};
|
||||
use imphnen_utils::AppError;
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use sea_orm::prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct PostgresOtpRepository {
|
||||
db: Arc<DatabaseConnection>,
|
||||
}
|
||||
|
||||
impl PostgresOtpRepository {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db: Arc::new(db) }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OtpRepository for PostgresOtpRepository {
|
||||
async fn save(&self, record: OtpCacheRecord) -> Result<(), AppError> {
|
||||
let now = Utc::now();
|
||||
let otp_hash = record.otp_hash.clone();
|
||||
let expires_at = record.expires_at;
|
||||
let active_model = OtpCacheActiveModel {
|
||||
id: Set(uuid::Uuid::new_v4()),
|
||||
email: Set(record.email.clone()),
|
||||
otp_hash: Set(otp_hash.clone()),
|
||||
expires_at: Set(expires_at),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
};
|
||||
|
||||
// Upsert: replace any existing (possibly expired) OTP for the same email.
|
||||
let existing = OtpCacheEntity::find()
|
||||
.filter(OtpCacheColumn::Email.eq(record.email.clone()))
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
|
||||
if let Some(existing) = existing {
|
||||
let mut update: OtpCacheActiveModel = existing.into();
|
||||
update.otp_hash = Set(otp_hash);
|
||||
update.expires_at = Set(expires_at);
|
||||
update.updated_at = Set(now);
|
||||
update
|
||||
.update(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
} else {
|
||||
OtpCacheEntity::insert(active_model)
|
||||
.exec(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn find_by_email(&self, email: &str) -> Result<OtpCacheRecord, AppError> {
|
||||
let row = OtpCacheEntity::find()
|
||||
.filter(OtpCacheColumn::Email.eq(email))
|
||||
.one(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?
|
||||
.ok_or_else(|| AppError::NotFoundError("No OTP issued for this email".into()))?;
|
||||
Ok(OtpCacheRecord {
|
||||
email: row.email,
|
||||
otp_hash: row.otp_hash,
|
||||
expires_at: row.expires_at,
|
||||
})
|
||||
}
|
||||
|
||||
async fn delete_by_email(&self, email: &str) -> Result<(), AppError> {
|
||||
OtpCacheEntity::delete_many()
|
||||
.filter(OtpCacheColumn::Email.eq(email))
|
||||
.exec(self.db.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user