wip
This commit is contained in:
239
src/objects/communication/verification.rs
Normal file
239
src/objects/communication/verification.rs
Normal file
@@ -0,0 +1,239 @@
|
||||
/// 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user