totp_challenge.rs 77 lignes · 2110 octets
use chrono::{DateTime, Utc};
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

/// TotpChallenge entity model
///
/// Ephemeral challenge for the 2FA login flow.
/// Created after successful password verification when user has TOTP enabled.
/// Expires after 5 minutes. Max 5 attempts per challenge.
#[derive(Clone, Debug, Serialize, Deserialize, DeriveEntityModel)]
#[sea_orm(table_name = "totp_challenges")]
pub struct Model {
    /// Unique identifier
    #[sea_orm(primary_key)]
    pub id: Uuid,
    /// User ID
    pub user_id: Uuid,
    /// Random challenge token (sent to client, used to identify the challenge)
    #[sea_orm(unique)]
    pub challenge_token: String,
    /// Number of verification attempts
    pub attempts: i32,
    /// Maximum allowed attempts
    pub max_attempts: i32,
    /// Challenge expiration time
    pub expires_at: DateTime<Utc>,
    /// Creation timestamp
    pub created_at: DateTime<Utc>,
}

/// SeaORM relations for the TotpChallenge entity.
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
    /// The user this challenge was issued for.
    #[sea_orm(
        belongs_to = "super::user::Entity",
        from = "Column::UserId",
        to = "super::user::Column::Id"
    )]
    User,
}

impl Related<super::user::Entity> for Entity {
    fn to() -> RelationDef {
        Relation::User.def()
    }
}

impl ActiveModelBehavior for ActiveModel {}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_totp_challenge_model() {
        let id = Uuid::new_v4();
        let user_id = Uuid::new_v4();
        let now = Utc::now();

        let challenge = Model {
            id,
            user_id,
            challenge_token: "test-token".to_string(),
            attempts: 0,
            max_attempts: 5,
            expires_at: now + chrono::Duration::minutes(5),
            created_at: now,
        };

        assert_eq!(challenge.id, id);
        assert_eq!(challenge.attempts, 0);
        assert_eq!(challenge.max_attempts, 5);
        assert!(challenge.expires_at > now);
    }
}