move repos into monorepo

This commit is contained in:
Timur Gordon
2025-11-13 20:44:00 +01:00
commit 4b23e5eb7f
204 changed files with 33737 additions and 0 deletions

View File

@@ -0,0 +1,489 @@
/// Email Client
///
/// Real SMTP email client for sending emails including verification emails.
use serde::{Deserialize, Serialize};
use super::verification::Verification;
use crate::store::{BaseData, Object, Storable};
use lettre::{
Message, SmtpTransport, Transport,
message::{header::ContentType, MultiPart, SinglePart},
transport::smtp::authentication::Credentials,
};
/// Email client with SMTP configuration
#[derive(Debug, Clone, Serialize, Deserialize, crate::DeriveObject)]
pub struct EmailClient {
#[serde(flatten)]
pub base_data: BaseData,
/// SMTP server hostname
pub smtp_host: String,
/// SMTP port
pub smtp_port: u16,
/// Username for SMTP auth
pub username: String,
/// Password for SMTP auth
pub password: String,
/// From address
pub from_address: String,
/// From name
pub from_name: String,
/// Use TLS
pub use_tls: bool,
}
/// Mail template with placeholders
#[derive(Debug, Clone, Serialize, Deserialize, crate::DeriveObject)]
pub struct MailTemplate {
#[serde(flatten)]
pub base_data: BaseData,
/// Template ID
pub id: String,
/// Template name
pub name: String,
/// Email subject (can contain placeholders like ${name})
pub subject: String,
/// Email body (can contain placeholders like ${code}, ${url})
pub body: String,
/// HTML body (optional, can contain placeholders)
pub html_body: Option<String>,
}
impl Default for MailTemplate {
fn default() -> Self {
Self {
base_data: BaseData::new(),
id: String::new(),
name: String::new(),
subject: String::new(),
body: String::new(),
html_body: None,
}
}
}
/// Email message created from a template
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Mail {
/// Recipient email address
pub to: String,
/// Template ID to use
pub template_id: Option<String>,
/// Parameters to replace in template
pub parameters: std::collections::HashMap<String, String>,
/// Direct subject (if not using template)
pub subject: Option<String>,
/// Direct body (if not using template)
pub body: Option<String>,
}
impl Default for EmailClient {
fn default() -> Self {
Self {
base_data: BaseData::new(),
smtp_host: "localhost".to_string(),
smtp_port: 587,
username: String::new(),
password: String::new(),
from_address: "noreply@example.com".to_string(),
from_name: "No Reply".to_string(),
use_tls: true,
}
}
}
impl MailTemplate {
/// Create a new mail template
pub fn new() -> Self {
Self::default()
}
/// Builder: Set template ID
pub fn id(mut self, id: String) -> Self {
self.id = id;
self
}
/// Builder: Set template name
pub fn name(mut self, name: String) -> Self {
self.name = name;
self
}
/// Builder: Set subject
pub fn subject(mut self, subject: String) -> Self {
self.subject = subject;
self
}
/// Builder: Set body
pub fn body(mut self, body: String) -> Self {
self.body = body;
self
}
/// Builder: Set HTML body
pub fn html_body(mut self, html_body: String) -> Self {
self.html_body = Some(html_body);
self
}
/// Replace placeholders in text
fn replace_placeholders(&self, text: &str, parameters: &std::collections::HashMap<String, String>) -> String {
let mut result = text.to_string();
for (key, value) in parameters {
let placeholder = format!("${{{}}}", key);
result = result.replace(&placeholder, value);
}
result
}
/// Render subject with parameters
pub fn render_subject(&self, parameters: &std::collections::HashMap<String, String>) -> String {
self.replace_placeholders(&self.subject, parameters)
}
/// Render body with parameters
pub fn render_body(&self, parameters: &std::collections::HashMap<String, String>) -> String {
self.replace_placeholders(&self.body, parameters)
}
/// Render HTML body with parameters
pub fn render_html_body(&self, parameters: &std::collections::HashMap<String, String>) -> Option<String> {
self.html_body.as_ref().map(|html| self.replace_placeholders(html, parameters))
}
}
impl Mail {
/// Create a new mail
pub fn new() -> Self {
Self::default()
}
/// Builder: Set recipient
pub fn to(mut self, to: String) -> Self {
self.to = to;
self
}
/// Builder: Set template ID
pub fn template(mut self, template_id: String) -> Self {
self.template_id = Some(template_id);
self
}
/// Builder: Add a parameter
pub fn parameter(mut self, key: String, value: String) -> Self {
self.parameters.insert(key, value);
self
}
/// Builder: Set subject (for non-template emails)
pub fn subject(mut self, subject: String) -> Self {
self.subject = Some(subject);
self
}
/// Builder: Set body (for non-template emails)
pub fn body(mut self, body: String) -> Self {
self.body = Some(body);
self
}
}
impl EmailClient {
/// Create a new email client
pub fn new() -> Self {
Self::default()
}
/// Builder: Set SMTP host
pub fn smtp_host(mut self, host: String) -> Self {
self.smtp_host = host;
self
}
/// Builder: Set SMTP port
pub fn smtp_port(mut self, port: u16) -> Self {
self.smtp_port = port;
self
}
/// Builder: Set username
pub fn username(mut self, username: String) -> Self {
self.username = username;
self
}
/// Builder: Set password
pub fn password(mut self, password: String) -> Self {
self.password = password;
self
}
/// Builder: Set from address
pub fn from_address(mut self, address: String) -> Self {
self.from_address = address;
self
}
/// Builder: Set from name
pub fn from_name(mut self, name: String) -> Self {
self.from_name = name;
self
}
/// Builder: Set use TLS
pub fn use_tls(mut self, use_tls: bool) -> Self {
self.use_tls = use_tls;
self
}
/// Build SMTP transport
fn build_transport(&self) -> Result<SmtpTransport, String> {
let creds = Credentials::new(
self.username.clone(),
self.password.clone(),
);
let transport = if self.use_tls {
SmtpTransport::starttls_relay(&self.smtp_host)
.map_err(|e| format!("Failed to create SMTP transport: {}", e))?
.credentials(creds)
.port(self.smtp_port)
.build()
} else {
SmtpTransport::builder_dangerous(&self.smtp_host)
.credentials(creds)
.port(self.smtp_port)
.build()
};
Ok(transport)
}
/// Send a plain text email
pub fn send_email(
&self,
to: &str,
subject: &str,
body: &str,
) -> Result<(), String> {
let from_mailbox = format!("{} <{}>", self.from_name, self.from_address)
.parse()
.map_err(|e| format!("Invalid from address: {}", e))?;
let to_mailbox = to.parse()
.map_err(|e| format!("Invalid to address: {}", e))?;
let email = Message::builder()
.from(from_mailbox)
.to(to_mailbox)
.subject(subject)
.body(body.to_string())
.map_err(|e| format!("Failed to build email: {}", e))?;
let transport = self.build_transport()?;
transport.send(&email)
.map_err(|e| format!("Failed to send email: {}", e))?;
Ok(())
}
/// Send an HTML email
pub fn send_html_email(
&self,
to: &str,
subject: &str,
html_body: &str,
text_body: Option<&str>,
) -> Result<(), String> {
let from_mailbox = format!("{} <{}>", self.from_name, self.from_address)
.parse()
.map_err(|e| format!("Invalid from address: {}", e))?;
let to_mailbox = to.parse()
.map_err(|e| format!("Invalid to address: {}", e))?;
// Build multipart email with text and HTML alternatives
let text_part = if let Some(text) = text_body {
SinglePart::builder()
.header(ContentType::TEXT_PLAIN)
.body(text.to_string())
} else {
SinglePart::builder()
.header(ContentType::TEXT_PLAIN)
.body(String::new())
};
let html_part = SinglePart::builder()
.header(ContentType::TEXT_HTML)
.body(html_body.to_string());
let multipart = MultiPart::alternative()
.singlepart(text_part)
.singlepart(html_part);
let email = Message::builder()
.from(from_mailbox)
.to(to_mailbox)
.subject(subject)
.multipart(multipart)
.map_err(|e| format!("Failed to build email: {}", e))?;
let transport = self.build_transport()?;
transport.send(&email)
.map_err(|e| format!("Failed to send email: {}", e))?;
Ok(())
}
/// Send a mail using a template
pub fn send_mail(&self, mail: &Mail, template: &MailTemplate) -> Result<(), String> {
// Render subject and body with parameters
let subject = template.render_subject(&mail.parameters);
let body_text = template.render_body(&mail.parameters);
let html_body = template.render_html_body(&mail.parameters);
// Send email
if let Some(html) = html_body {
self.send_html_email(&mail.to, &subject, &html, Some(&body_text))
} else {
self.send_email(&mail.to, &subject, &body_text)
}
}
/// Send a verification email with code
pub fn send_verification_code_email(
&self,
verification: &Verification,
) -> Result<(), String> {
let subject = "Verify your email address";
let body = format!(
"Hello,\n\n\
Please verify your email address by entering this code:\n\n\
{}\n\n\
This code will expire in 24 hours.\n\n\
If you didn't request this, please ignore this email.",
verification.verification_code
);
self.send_email(&verification.contact, subject, &body)
}
/// Send a verification email with URL link
pub fn send_verification_link_email(
&self,
verification: &Verification,
) -> Result<(), String> {
let verification_url = verification.get_verification_url()
.ok_or_else(|| "No callback URL configured".to_string())?;
let subject = "Verify your email address";
let html_body = format!(
r#"<!DOCTYPE html>
<html>
<head>
<style>
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
.button {{
display: inline-block;
padding: 12px 24px;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 4px;
margin: 20px 0;
}}
.code {{
font-size: 24px;
font-weight: bold;
letter-spacing: 4px;
padding: 10px;
background-color: #f5f5f5;
display: inline-block;
margin: 10px 0;
}}
</style>
</head>
<body>
<div class="container">
<h2>Verify your email address</h2>
<p>Hello,</p>
<p>Please verify your email address by clicking the button below:</p>
<a href="{}" class="button">Verify Email</a>
<p>Or enter this verification code:</p>
<div class="code">{}</div>
<p>This link and code will expire in 24 hours.</p>
<p>If you didn't request this, please ignore this email.</p>
</div>
</body>
</html>"#,
verification_url, verification.verification_code
);
let text_body = format!(
"Hello,\n\n\
Please verify your email address by visiting this link:\n\
{}\n\n\
Or enter this verification code: {}\n\n\
This link and code will expire in 24 hours.\n\n\
If you didn't request this, please ignore this email.",
verification_url, verification.verification_code
);
self.send_html_email(
&verification.contact,
subject,
&html_body,
Some(&text_body),
)
}
}
// For Rhai integration, we need a simpler synchronous wrapper
impl EmailClient {
/// Synchronous wrapper for send_verification_code_email
pub fn send_verification_code_sync(&self, verification: &Verification) -> Result<(), String> {
// In a real implementation, you'd use tokio::runtime::Runtime::new().block_on()
// For now, just simulate
println!("=== VERIFICATION CODE EMAIL ===");
println!("To: {}", verification.contact);
println!("Code: {}", verification.verification_code);
println!("===============================");
Ok(())
}
/// Synchronous wrapper for send_verification_link_email
pub fn send_verification_link_sync(&self, verification: &Verification) -> Result<(), String> {
let verification_url = verification.get_verification_url()
.ok_or_else(|| "No callback URL configured".to_string())?;
println!("=== VERIFICATION LINK EMAIL ===");
println!("To: {}", verification.contact);
println!("Code: {}", verification.verification_code);
println!("Link: {}", verification_url);
println!("===============================");
Ok(())
}
}

View File

@@ -0,0 +1,10 @@
/// Communication Module
///
/// Transport-agnostic verification and email client.
pub mod verification;
pub mod email;
pub mod rhai;
pub use verification::{Verification, VerificationStatus, VerificationTransport};
pub use email::EmailClient;

View File

@@ -0,0 +1,407 @@
/// Rhai bindings for Communication (Verification and Email)
use ::rhai::plugin::*;
use ::rhai::{CustomType, Dynamic, Engine, EvalAltResult, Module, TypeBuilder};
use super::verification::{Verification, VerificationStatus, VerificationTransport};
use super::email::{EmailClient, MailTemplate, Mail};
// ============================================================================
// Verification Module
// ============================================================================
type RhaiVerification = Verification;
#[export_module]
mod rhai_verification_module {
use super::RhaiVerification;
use super::super::verification::{Verification, VerificationTransport};
#[rhai_fn(name = "new_verification", return_raw)]
pub fn new_verification(
entity_id: String,
contact: String,
) -> Result<RhaiVerification, Box<EvalAltResult>> {
// Default to email transport
Ok(Verification::new(0, entity_id, contact, VerificationTransport::Email))
}
#[rhai_fn(name = "callback_url", return_raw)]
pub fn set_callback_url(
verification: &mut RhaiVerification,
url: String,
) -> Result<RhaiVerification, Box<EvalAltResult>> {
let owned = std::mem::take(verification);
*verification = owned.callback_url(url);
Ok(verification.clone())
}
#[rhai_fn(name = "mark_sent", return_raw)]
pub fn mark_sent(
verification: &mut RhaiVerification,
) -> Result<(), Box<EvalAltResult>> {
verification.mark_sent();
Ok(())
}
#[rhai_fn(name = "verify_code", return_raw)]
pub fn verify_code(
verification: &mut RhaiVerification,
code: String,
) -> Result<(), Box<EvalAltResult>> {
verification.verify_code(&code)
.map_err(|e| e.into())
}
#[rhai_fn(name = "verify_nonce", return_raw)]
pub fn verify_nonce(
verification: &mut RhaiVerification,
nonce: String,
) -> Result<(), Box<EvalAltResult>> {
verification.verify_nonce(&nonce)
.map_err(|e| e.into())
}
#[rhai_fn(name = "resend", return_raw)]
pub fn resend(
verification: &mut RhaiVerification,
) -> Result<(), Box<EvalAltResult>> {
verification.resend();
Ok(())
}
// Getters
#[rhai_fn(name = "get_entity_id")]
pub fn get_entity_id(verification: &mut RhaiVerification) -> String {
verification.entity_id.clone()
}
#[rhai_fn(name = "get_contact")]
pub fn get_contact(verification: &mut RhaiVerification) -> String {
verification.contact.clone()
}
#[rhai_fn(name = "get_code")]
pub fn get_code(verification: &mut RhaiVerification) -> String {
verification.verification_code.clone()
}
#[rhai_fn(name = "get_nonce")]
pub fn get_nonce(verification: &mut RhaiVerification) -> String {
verification.verification_nonce.clone()
}
#[rhai_fn(name = "get_verification_url")]
pub fn get_verification_url(verification: &mut RhaiVerification) -> String {
verification.get_verification_url().unwrap_or_default()
}
#[rhai_fn(name = "get_status")]
pub fn get_status(verification: &mut RhaiVerification) -> String {
format!("{:?}", verification.status)
}
#[rhai_fn(name = "get_attempts")]
pub fn get_attempts(verification: &mut RhaiVerification) -> i64 {
verification.attempts as i64
}
}
// ============================================================================
// Mail Template Module
// ============================================================================
type RhaiMailTemplate = MailTemplate;
#[export_module]
mod rhai_mail_template_module {
use super::RhaiMailTemplate;
use super::super::email::MailTemplate;
use ::rhai::EvalAltResult;
#[rhai_fn(name = "new_mail_template", return_raw)]
pub fn new_mail_template() -> Result<RhaiMailTemplate, Box<EvalAltResult>> {
Ok(MailTemplate::new())
}
#[rhai_fn(name = "id", return_raw)]
pub fn set_id(
template: &mut RhaiMailTemplate,
id: String,
) -> Result<RhaiMailTemplate, Box<EvalAltResult>> {
let owned = std::mem::take(template);
*template = owned.id(id);
Ok(template.clone())
}
#[rhai_fn(name = "name", return_raw)]
pub fn set_name(
template: &mut RhaiMailTemplate,
name: String,
) -> Result<RhaiMailTemplate, Box<EvalAltResult>> {
let owned = std::mem::take(template);
*template = owned.name(name);
Ok(template.clone())
}
#[rhai_fn(name = "subject", return_raw)]
pub fn set_subject(
template: &mut RhaiMailTemplate,
subject: String,
) -> Result<RhaiMailTemplate, Box<EvalAltResult>> {
let owned = std::mem::take(template);
*template = owned.subject(subject);
Ok(template.clone())
}
#[rhai_fn(name = "body", return_raw)]
pub fn set_body(
template: &mut RhaiMailTemplate,
body: String,
) -> Result<RhaiMailTemplate, Box<EvalAltResult>> {
let owned = std::mem::take(template);
*template = owned.body(body);
Ok(template.clone())
}
#[rhai_fn(name = "html_body", return_raw)]
pub fn set_html_body(
template: &mut RhaiMailTemplate,
html_body: String,
) -> Result<RhaiMailTemplate, Box<EvalAltResult>> {
let owned = std::mem::take(template);
*template = owned.html_body(html_body);
Ok(template.clone())
}
// Getters
#[rhai_fn(name = "get_id")]
pub fn get_id(template: &mut RhaiMailTemplate) -> String {
template.id.clone()
}
}
// ============================================================================
// Mail Module
// ============================================================================
type RhaiMail = Mail;
#[export_module]
mod rhai_mail_module {
use super::RhaiMail;
use super::super::email::Mail;
use ::rhai::EvalAltResult;
#[rhai_fn(name = "new_mail", return_raw)]
pub fn new_mail() -> Result<RhaiMail, Box<EvalAltResult>> {
Ok(Mail::new())
}
#[rhai_fn(name = "to", return_raw)]
pub fn set_to(
mail: &mut RhaiMail,
to: String,
) -> Result<RhaiMail, Box<EvalAltResult>> {
let owned = std::mem::take(mail);
*mail = owned.to(to);
Ok(mail.clone())
}
#[rhai_fn(name = "template", return_raw)]
pub fn set_template(
mail: &mut RhaiMail,
template_id: String,
) -> Result<RhaiMail, Box<EvalAltResult>> {
let owned = std::mem::take(mail);
*mail = owned.template(template_id);
Ok(mail.clone())
}
#[rhai_fn(name = "parameter", return_raw)]
pub fn add_parameter(
mail: &mut RhaiMail,
key: String,
value: String,
) -> Result<RhaiMail, Box<EvalAltResult>> {
let owned = std::mem::take(mail);
*mail = owned.parameter(key, value);
Ok(mail.clone())
}
}
// ============================================================================
// Email Client Module
// ============================================================================
type RhaiEmailClient = EmailClient;
#[export_module]
mod rhai_email_module {
use super::RhaiEmailClient;
use super::RhaiMail;
use super::RhaiMailTemplate;
use super::super::email::{EmailClient, Mail, MailTemplate};
use super::super::verification::Verification;
use ::rhai::EvalAltResult;
#[rhai_fn(name = "new_email_client", return_raw)]
pub fn new_email_client() -> Result<RhaiEmailClient, Box<EvalAltResult>> {
Ok(EmailClient::new())
}
#[rhai_fn(name = "smtp_host", return_raw)]
pub fn set_smtp_host(
client: &mut RhaiEmailClient,
host: String,
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
let owned = std::mem::take(client);
*client = owned.smtp_host(host);
Ok(client.clone())
}
#[rhai_fn(name = "smtp_port", return_raw)]
pub fn set_smtp_port(
client: &mut RhaiEmailClient,
port: i64,
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
let owned = std::mem::take(client);
*client = owned.smtp_port(port as u16);
Ok(client.clone())
}
#[rhai_fn(name = "username", return_raw)]
pub fn set_username(
client: &mut RhaiEmailClient,
username: String,
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
let owned = std::mem::take(client);
*client = owned.username(username);
Ok(client.clone())
}
#[rhai_fn(name = "password", return_raw)]
pub fn set_password(
client: &mut RhaiEmailClient,
password: String,
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
let owned = std::mem::take(client);
*client = owned.password(password);
Ok(client.clone())
}
#[rhai_fn(name = "from_email", return_raw)]
pub fn set_from_email(
client: &mut RhaiEmailClient,
email: String,
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
let owned = std::mem::take(client);
*client = owned.from_address(email);
Ok(client.clone())
}
#[rhai_fn(name = "from_name", return_raw)]
pub fn set_from_name(
client: &mut RhaiEmailClient,
name: String,
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
let owned = std::mem::take(client);
*client = owned.from_name(name);
Ok(client.clone())
}
#[rhai_fn(name = "use_tls", return_raw)]
pub fn set_use_tls(
client: &mut RhaiEmailClient,
use_tls: bool,
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
let owned = std::mem::take(client);
*client = owned.use_tls(use_tls);
Ok(client.clone())
}
#[rhai_fn(name = "send_mail", return_raw)]
pub fn send_mail(
client: &mut RhaiEmailClient,
mail: RhaiMail,
template: RhaiMailTemplate,
) -> Result<(), Box<EvalAltResult>> {
client.send_mail(&mail, &template)
.map_err(|e| e.into())
}
#[rhai_fn(name = "send_verification_code", return_raw)]
pub fn send_verification_code(
client: &mut RhaiEmailClient,
verification: Verification,
) -> Result<(), Box<EvalAltResult>> {
client.send_verification_code_sync(&verification)
.map_err(|e| e.into())
}
#[rhai_fn(name = "send_verification_link", return_raw)]
pub fn send_verification_link(
client: &mut RhaiEmailClient,
verification: Verification,
) -> Result<(), Box<EvalAltResult>> {
client.send_verification_link_sync(&verification)
.map_err(|e| e.into())
}
}
// ============================================================================
// Registration Functions
// ============================================================================
/// Register Communication modules into a Rhai Module
pub fn register_communication_modules(parent_module: &mut Module) {
// Register custom types
parent_module.set_custom_type::<Verification>("Verification");
parent_module.set_custom_type::<MailTemplate>("MailTemplate");
parent_module.set_custom_type::<Mail>("Mail");
parent_module.set_custom_type::<EmailClient>("EmailClient");
// Merge verification functions
let verification_module = exported_module!(rhai_verification_module);
parent_module.combine_flatten(verification_module);
// Merge mail template functions
let mail_template_module = exported_module!(rhai_mail_template_module);
parent_module.combine_flatten(mail_template_module);
// Merge mail functions
let mail_module = exported_module!(rhai_mail_module);
parent_module.combine_flatten(mail_module);
// Merge email client functions
let email_module = exported_module!(rhai_email_module);
parent_module.combine_flatten(email_module);
}
// ============================================================================
// CustomType Implementations
// ============================================================================
impl CustomType for Verification {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("Verification");
}
}
impl CustomType for MailTemplate {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("MailTemplate");
}
}
impl CustomType for Mail {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("Mail");
}
}
impl CustomType for EmailClient {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("EmailClient");
}
}

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

View File

@@ -0,0 +1,155 @@
/// Email Verification
///
/// Manages email verification sessions and status.
use crate::store::{BaseData, Object, Storable};
use serde::{Deserialize, Serialize};
/// Email verification status
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "lowercase")]
pub enum VerificationStatus {
#[default]
Pending,
Sent,
Verified,
Expired,
Failed,
}
/// Email Verification Session
#[derive(Debug, Clone, Serialize, Deserialize, Default, crate::DeriveObject)]
pub struct EmailVerification {
#[serde(flatten)]
pub base_data: BaseData,
/// User/entity ID this verification is for
pub entity_id: String,
/// Email address to verify
pub email: String,
/// Verification code/token
pub verification_code: 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,
/// Additional metadata
#[serde(default)]
pub metadata: std::collections::HashMap<String, String>,
}
impl EmailVerification {
/// Create a new email verification
pub fn new(id: u32, entity_id: String, email: String) -> Self {
let mut base_data = BaseData::new();
base_data.id = id;
// Generate verification code (6 digits)
let code = Self::generate_code();
// Set expiry to 24 hours from now
let expires_at = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() + (24 * 60 * 60);
Self {
base_data,
entity_id,
email,
verification_code: code,
status: VerificationStatus::Pending,
sent_at: None,
verified_at: None,
expires_at: Some(expires_at),
attempts: 0,
max_attempts: 3,
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)
}
/// 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(&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(())
}
/// Resend verification (generate new code)
pub fn resend(&mut self) {
self.verification_code = Self::generate_code();
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()
}
}