This repository has been archived on 2025-11-14. You can view files and clone it, but cannot push or open issues or pull requests.
Files
osiris_old/src/objects/communication/verification.rs
Timur Gordon 87c556df7a wip
2025-10-29 16:52:33 +01:00

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()
}
}