use base64::Engine; use serde::Deserialize; #[derive(Debug, Clone, Deserialize)] #[serde(default)] pub struct PasswordPolicy { pub min_length: usize, pub require_upper: bool, pub require_lower: bool, pub require_digit: bool, pub require_symbol: bool, } impl Default for PasswordPolicy { fn default() -> Self { Self { min_length: 8, require_upper: true, require_lower: true, require_digit: true, require_symbol: false, } } } #[derive(Debug, Clone, Default, Deserialize)] #[serde(default)] pub struct AgreementDocConfig { pub key: String, pub version: String, pub title: String, pub content_md: String, } #[derive(Debug, Clone, Deserialize)] #[serde(default)] pub struct AuthConfig { pub password_policy: PasswordPolicy, pub verify_code_ttl_secs: u64, pub verify_code_len: usize, pub reset_token_ttl_secs: u64, pub reset_token_bytes: usize, pub email_code_rate_limit_secs: u64, pub max_verify_attempts: u64, pub min_age: u8, pub totp_encryption_key: Option, pub agreements: Vec, } impl Default for AuthConfig { fn default() -> Self { Self { password_policy: PasswordPolicy::default(), verify_code_ttl_secs: 600, verify_code_len: 6, reset_token_ttl_secs: 1800, reset_token_bytes: 32, email_code_rate_limit_secs: 60, max_verify_attempts: 5, min_age: 13, totp_encryption_key: None, agreements: vec![ AgreementDocConfig { key: "terms".to_string(), version: "0.1.0".to_string(), title: "Terms of Service".to_string(), content_md: "Placeholder terms of service.".to_string(), }, AgreementDocConfig { key: "privacy".to_string(), version: "0.1.0".to_string(), title: "Privacy Policy".to_string(), content_md: "Placeholder privacy policy.".to_string(), }, ], } } } impl AuthConfig { pub fn new() -> Self { Self::default() } pub fn with_password_policy(mut self, policy: PasswordPolicy) -> Self { self.password_policy = policy; self } pub fn with_verify_code_ttl_secs(mut self, secs: u64) -> Self { self.verify_code_ttl_secs = secs; self } pub fn with_verify_code_len(mut self, len: usize) -> Self { self.verify_code_len = len; self } pub fn with_reset_token_ttl_secs(mut self, secs: u64) -> Self { self.reset_token_ttl_secs = secs; self } pub fn with_reset_token_bytes(mut self, bytes: usize) -> Self { self.reset_token_bytes = bytes; self } pub fn with_email_code_rate_limit_secs(mut self, secs: u64) -> Self { self.email_code_rate_limit_secs = secs; self } pub fn with_max_verify_attempts(mut self, attempts: u64) -> Self { self.max_verify_attempts = attempts; self } pub fn with_min_age(mut self, min_age: u8) -> Self { self.min_age = min_age; self } pub fn with_totp_encryption_key(mut self, key: impl Into) -> Self { self.totp_encryption_key = Some(key.into()); self } pub fn with_agreements(mut self, agreements: Vec) -> Self { self.agreements = agreements; self } pub fn validate(&self) -> Result<(), String> { if !(4..=10).contains(&self.verify_code_len) { return Err("auth.verify_code_len must be between 4 and 10".to_string()); } if self.verify_code_ttl_secs == 0 { return Err("auth.verify_code_ttl_secs must be greater than 0".to_string()); } if self.reset_token_ttl_secs == 0 { return Err("auth.reset_token_ttl_secs must be greater than 0".to_string()); } if self.reset_token_bytes < 16 { return Err("auth.reset_token_bytes must be at least 16".to_string()); } if self.email_code_rate_limit_secs == 0 { return Err("auth.email_code_rate_limit_secs must be greater than 0".to_string()); } if self.max_verify_attempts == 0 { return Err("auth.max_verify_attempts must be greater than 0".to_string()); } if let Some(key) = &self.totp_encryption_key { let length = base64::engine::general_purpose::STANDARD .decode(key) .map(|bytes| bytes.len()) .unwrap_or(0); if length < 32 { return Err( "auth.totp_encryption_key must be base64 of at least 32 bytes".to_string(), ); } } Ok(()) } }