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); } }
GitRust