user_totp.rs 90 lignes · 2813 octets
use chrono::{DateTime, Utc};
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

/// UserTotp entity model
///
/// Stores TOTP two-factor authentication data for a user.
/// One-to-one relationship with User (enforced by unique constraint on user_id).
/// The secret and backup codes are encrypted at rest via CryptoService (AES-256-GCM).
#[derive(Clone, Debug, Serialize, Deserialize, DeriveEntityModel)]
#[sea_orm(table_name = "user_totp")]
pub struct Model {
    /// Unique identifier
    #[sea_orm(primary_key)]
    pub id: Uuid,
    /// User ID (unique, one-to-one with users)
    #[sea_orm(unique)]
    pub user_id: Uuid,
    /// TOTP secret encrypted with AES-256-GCM (enc: prefixed)
    pub secret_encrypted: String,
    /// Whether 2FA is fully enabled (true after first code verification)
    pub enabled: bool,
    /// Backup codes: JSON array of bcrypt-hashed codes, AES-encrypted
    pub backup_codes_encrypted: Option<String>,
    /// Number of remaining unused backup codes
    pub backup_codes_remaining: i32,
    /// Timestamp when 2FA was enabled
    pub enabled_at: Option<DateTime<Utc>>,
    /// Last TOTP time-step (`unix_epoch / 30`) that was successfully consumed.
    ///
    /// `None` until the first accepted code after this column exists.
    /// Updated through a conditional compare-and-set `UPDATE` so the same
    /// step is rejected on every replica (anti-replay, RFC 0037).
    pub last_used_step: Option<i64>,
    /// Creation timestamp
    pub created_at: DateTime<Utc>,
    /// Last update timestamp
    pub updated_at: DateTime<Utc>,
}

/// SeaORM relations for the UserTotp entity.
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
    /// The user these TOTP credentials belong to.
    #[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_user_totp_model() {
        let id = Uuid::new_v4();
        let user_id = Uuid::new_v4();
        let now = Utc::now();

        let totp = Model {
            id,
            user_id,
            secret_encrypted: "enc:test".to_string(),
            enabled: false,
            backup_codes_encrypted: None,
            backup_codes_remaining: 0,
            enabled_at: None,
            last_used_step: None,
            created_at: now,
            updated_at: now,
        };

        assert_eq!(totp.id, id);
        assert_eq!(totp.user_id, user_id);
        assert!(!totp.enabled);
        assert_eq!(totp.backup_codes_remaining, 0);
    }
}