240 lines
7.0 KiB
Rust
240 lines
7.0 KiB
Rust
/// 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<u64>,
|
|
|
|
/// When verification was completed
|
|
pub verified_at: Option<u64>,
|
|
|
|
/// When verification expires
|
|
pub expires_at: Option<u64>,
|
|
|
|
/// 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<String>,
|
|
|
|
/// Additional metadata
|
|
#[serde(default)]
|
|
pub metadata: std::collections::HashMap<String, String>,
|
|
}
|
|
|
|
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<String> {
|
|
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()
|
|
}
|
|
}
|