/// Transport-Agnostic Verification /// /// Manages verification sessions with codes and nonces for email, SMS, etc. use crate::store::{BaseData, Object, Storable}; use serde::{Deserialize, Serialize}; /// Verification transport type #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum VerificationTransport { Email, Sms, WhatsApp, Telegram, Other(String), } impl Default for VerificationTransport { fn default() -> Self { VerificationTransport::Email } } /// Verification status #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] #[serde(rename_all = "lowercase")] pub enum VerificationStatus { #[default] Pending, Sent, Verified, Expired, Failed, } /// Verification Session /// /// Transport-agnostic verification that can be used for email, SMS, etc. /// Supports both code-based verification and URL-based (nonce) verification. #[derive(Debug, Clone, Serialize, Deserialize, Default, crate::DeriveObject)] pub struct Verification { #[serde(flatten)] pub base_data: BaseData, /// User/entity ID this verification is for pub entity_id: String, /// Contact address (email, phone, etc.) pub contact: String, /// Transport type pub transport: VerificationTransport, /// Verification code (6 digits for user entry) pub verification_code: String, /// Verification nonce (for URL-based verification) pub verification_nonce: String, /// Current status pub status: VerificationStatus, /// When verification was sent pub sent_at: Option, /// When verification was completed pub verified_at: Option, /// When verification expires pub expires_at: Option, /// Number of attempts pub attempts: u32, /// Maximum attempts allowed pub max_attempts: u32, /// Callback URL (for server to construct verification link) pub callback_url: Option, /// Additional metadata #[serde(default)] pub metadata: std::collections::HashMap, } impl Verification { /// Create a new verification pub fn new(id: u32, entity_id: String, contact: String, transport: VerificationTransport) -> Self { let mut base_data = BaseData::new(); base_data.id = id; // Generate verification code (6 digits) let code = Self::generate_code(); // Generate verification nonce (32 char hex) let nonce = Self::generate_nonce(); // Set expiry to 24 hours from now let expires_at = Self::now() + (24 * 60 * 60); Self { base_data, entity_id, contact, transport, verification_code: code, verification_nonce: nonce, status: VerificationStatus::Pending, sent_at: None, verified_at: None, expires_at: Some(expires_at), attempts: 0, max_attempts: 3, callback_url: None, metadata: std::collections::HashMap::new(), } } /// Generate a 6-digit verification code fn generate_code() -> String { use std::time::{SystemTime, UNIX_EPOCH}; let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_nanos(); format!("{:06}", (timestamp % 1_000_000) as u32) } /// Generate a verification nonce (32 char hex string) fn generate_nonce() -> String { use std::time::{SystemTime, UNIX_EPOCH}; let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_nanos(); format!("{:032x}", timestamp) } /// Set callback URL pub fn callback_url(mut self, url: String) -> Self { self.callback_url = Some(url); self } /// Get verification URL (callback_url + nonce) pub fn get_verification_url(&self) -> Option { self.callback_url.as_ref().map(|base_url| { if base_url.contains('?') { format!("{}&nonce={}", base_url, self.verification_nonce) } else { format!("{}?nonce={}", base_url, self.verification_nonce) } }) } /// Mark as sent pub fn mark_sent(&mut self) { self.status = VerificationStatus::Sent; self.sent_at = Some(Self::now()); self.base_data.update_modified(); } /// Verify with code pub fn verify_code(&mut self, code: &str) -> Result<(), String> { // Check if expired if let Some(expires_at) = self.expires_at { if Self::now() > expires_at { self.status = VerificationStatus::Expired; self.base_data.update_modified(); return Err("Verification code expired".to_string()); } } // Check attempts self.attempts += 1; if self.attempts > self.max_attempts { self.status = VerificationStatus::Failed; self.base_data.update_modified(); return Err("Maximum attempts exceeded".to_string()); } // Check code if code != self.verification_code { self.base_data.update_modified(); return Err("Invalid verification code".to_string()); } // Success self.status = VerificationStatus::Verified; self.verified_at = Some(Self::now()); self.base_data.update_modified(); Ok(()) } /// Verify with nonce (for URL-based verification) pub fn verify_nonce(&mut self, nonce: &str) -> Result<(), String> { // Check if expired if let Some(expires_at) = self.expires_at { if Self::now() > expires_at { self.status = VerificationStatus::Expired; self.base_data.update_modified(); return Err("Verification link expired".to_string()); } } // Check nonce if nonce != self.verification_nonce { self.base_data.update_modified(); return Err("Invalid verification link".to_string()); } // Success self.status = VerificationStatus::Verified; self.verified_at = Some(Self::now()); self.base_data.update_modified(); Ok(()) } /// Resend verification (generate new code and nonce) pub fn resend(&mut self) { self.verification_code = Self::generate_code(); self.verification_nonce = Self::generate_nonce(); self.status = VerificationStatus::Pending; self.attempts = 0; // Extend expiry self.expires_at = Some(Self::now() + (24 * 60 * 60)); self.base_data.update_modified(); } /// Helper to get current timestamp fn now() -> u64 { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs() } }